/*
 * Copyright 2010-2020 Alfresco Software, Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.activiti.core.el.juel.tree.impl.ast;

import jakarta.el.ELContext;
import jakarta.el.ELException;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.activiti.core.el.juel.misc.LocalMessages;
import org.activiti.core.el.juel.tree.Bindings;
import org.activiti.core.el.juel.tree.FunctionNode;

public class AstFunction extends AstRightValue implements FunctionNode {

    private final int index;
    private final String name;
    private final AstParameters params;
    private final boolean varargs;

    public AstFunction(String name, int index, AstParameters params) {
        this(name, index, params, false);
    }

    public AstFunction(
        String name,
        int index,
        AstParameters params,
        boolean varargs
    ) {
        this.name = name;
        this.index = index;
        this.params = params;
        this.varargs = varargs;
    }

    /**
     * Invoke method.
     * @param bindings
     * @param context
     * @param base
     * @param method
     * @return method result
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     */
    protected Object invoke(
        Bindings bindings,
        ELContext context,
        Object base,
        Method method
    ) throws InvocationTargetException, IllegalAccessException {
        Class<?>[] types = method.getParameterTypes();
        Object[] params = null;
        if (types.length > 0) {
            params = new Object[types.length];
            if (varargs && method.isVarArgs()) {
                for (int i = 0; i < params.length - 1; i++) {
                    Object param = getParam(i).eval(bindings, context);
                    if (param != null || types[i].isPrimitive()) {
                        params[i] = bindings.convert(param, types[i]);
                    }
                }
                int varargIndex = types.length - 1;
                Class<?> varargType = types[varargIndex].getComponentType();
                int length = getParamCount() - varargIndex;
                Object array = null;
                if (length == 1) { // special: eventually use argument as is
                    Object param = getParam(varargIndex)
                        .eval(bindings, context);
                    if (param != null && param.getClass().isArray()) {
                        if (types[varargIndex].isInstance(param)) {
                            array = param;
                        } else { // coerce array elements
                            length = Array.getLength(param);
                            array = Array.newInstance(varargType, length);
                            for (int i = 0; i < length; i++) {
                                Object elem = Array.get(param, i);
                                if (elem != null || varargType.isPrimitive()) {
                                    Array.set(
                                        array,
                                        i,
                                        bindings.convert(elem, varargType)
                                    );
                                }
                            }
                        }
                    } else { // single element array
                        array = Array.newInstance(varargType, 1);
                        if (param != null || varargType.isPrimitive()) {
                            Array.set(
                                array,
                                0,
                                bindings.convert(param, varargType)
                            );
                        }
                    }
                } else {
                    array = Array.newInstance(varargType, length);
                    for (int i = 0; i < length; i++) {
                        Object param = getParam(varargIndex + i)
                            .eval(bindings, context);
                        if (param != null || varargType.isPrimitive()) {
                            Array.set(
                                array,
                                i,
                                bindings.convert(param, varargType)
                            );
                        }
                    }
                }
                params[varargIndex] = array;
            } else {
                for (int i = 0; i < params.length; i++) {
                    Object param = getParam(i).eval(bindings, context);
                    if (param != null || types[i].isPrimitive()) {
                        params[i] = bindings.convert(param, types[i]);
                    }
                }
            }
        }
        return method.invoke(base, params);
    }

    @Override
    public Object eval(Bindings bindings, ELContext context) {
        Method method = bindings.getFunction(index);
        try {
            return invoke(bindings, context, null, method);
        } catch (IllegalAccessException e) {
            throw new ELException(
                LocalMessages.get("error.function.access", name),
                e
            );
        } catch (InvocationTargetException e) {
            throw new ELException(
                LocalMessages.get("error.function.invocation", name),
                e.getCause()
            );
        }
    }

    @Override
    public String toString() {
        return name;
    }

    @Override
    public void appendStructure(StringBuilder b, Bindings bindings) {
        b.append(
            bindings != null && bindings.isFunctionBound(index) ? "<fn>" : name
        );
        params.appendStructure(b, bindings);
    }

    public int getIndex() {
        return index;
    }

    public String getName() {
        return name;
    }

    public boolean isVarArgs() {
        return varargs;
    }

    public int getParamCount() {
        return params.getCardinality();
    }

    protected AstNode getParam(int i) {
        return params.getChild(i);
    }

    public int getCardinality() {
        return 1;
    }

    public AstNode getChild(int i) {
        return i == 0 ? params : null;
    }
}
